Por lo que sé, la implementación de cpython mantiene el mismo objeto para algunos valores iguales para ahorrar memoria. Por ejemplo, cuando creo 2 cadenas con el valor hello , cpython no crea 2 PyObject diferentes:
>>> s1 = 'hello' >>> s2 = 'hello' >>> s1 is s2 TrueMe enteré de ello con el nombre string interning . Cuando traté de verificarlo con otros tipos de python, observé que casi todos los tipos hashable (inmutables) son iguales:
>>> int() is int() True >>> str() is str() True >>> frozenset() is frozenset() True >>> bool() is bool() True Y casi todos los tipos mutables son opuestos (cpython crea un nuevo PyObject incluso para los mismos valores):
>>> list() is list() False >>> set() is set() False >>> dict() is dict() False Y creo que es porque podemos tener el mismo PyObject para objetos inmutables sin tener ningún problema.
Mi pregunta surge cuando veo que el tipo float se comporta de manera diferente a otros tipos inmutables:
>>> float() is float() False¿Por qué es diferente?
Los objetos mutables siempre crean un nuevo objeto; de lo contrario, los datos se compartirían. No hay mucho que explicar aquí, ya que si agrega un elemento a una lista vacía, no desea que todas las listas vacías tengan ese elemento.
Los objetos inmutables se comportan de una manera completamente diferente:
Las cuerdas se internan . Si tienen menos de 20 caracteres alfanuméricos y son estáticos (constantes en el código, nombres de funciones, etc.), se almacenan en caché y se accede a ellos desde un mapeo especial reservado para estos. Es para ahorrar memoria, pero lo que es más importante, se usa para tener una comparación más rápida. Python usa muchas operaciones de acceso al diccionario bajo el capó que requieren comparación de cadenas. Ser capaz de comparar 2 cadenas como atributos o nombres de funciones comparando su dirección de memoria en lugar del valor real es una mejora significativa en el tiempo de ejecución.
Los booleanos simplemente devuelven el mismo objeto . Teniendo en cuenta que solo hay 2 disponibles, no tiene sentido crearlos una y otra vez.
Los números enteros pequeños (de -5 a 256) de forma predeterminada también se almacenan en caché . Estos se utilizan con bastante frecuencia, en casi todas partes. Cada vez que un número entero está en ese rango, CPython simplemente devuelve el mismo objeto.
Sin embargo, los flotadores no se almacenan en caché. A diferencia de los números enteros, donde los números 0-10 son extremadamente comunes, no se garantiza que 1.0 se use más que 2.0 o 0.1 . Es por eso que float() simplemente devuelve un nuevo flotante. Podríamos haber optimizado el float() vacío, y podemos verificar los beneficios de velocidad, pero podría no haber hecho tanta diferencia.
La confusión comienza a surgir cuando float(0.0) is float(0.0) . Python tiene numerosas optimizaciones integradas:
En primer lugar, las constantes se guardan en el objeto de código de cada función. 0.0 is 0.0 simplemente se refiere al mismo objeto. Es una optimización en tiempo de compilación.
En segundo lugar, float(0.0) toma el objeto 0.0 y, dado que es un objeto flotante (que es inmutable), simplemente lo devuelve . No es necesario crear un nuevo objeto si ya es un flotador.
Por último, 1.0 + 1.0 is 2.0 también funcionará. La razón es que 1.0 + 1.0 se calcula en tiempo de compilación y luego hace referencia al mismo objeto 2.0 :
def test(): return 1.0 + 1.0 is 2.0 dis.dis(test) 2 0 LOAD_CONST 1 (2.0) 2 LOAD_CONST 1 (2.0) 4 IS_OP 0 6 RETURN_VALUEComo puede ver, no hay operación de suma. La función se compiló con el resultado apuntando exactamente al mismo objeto constante.
Entonces, si bien no hay una optimización específica de flotación, están en juego 3 optimizaciones genéricas diferentes. La suma de ellos es lo que finalmente decide si será el mismo objeto o no.